// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.multiwindow;

import static org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper.createSecondChromeTabbedActivity;

import android.app.Activity;
import android.content.Intent;
import android.os.Build;

import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisableIf;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.ChromeTabbedActivity2;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.content_public.browser.test.util.TestThreadUtils;

import java.util.concurrent.TimeoutException;

/**
 * Class for testing MultiWindowUtils.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@MinAndroidSdkLevel(Build.VERSION_CODES.N)
public class MultiWindowUtilsTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Before
    public void setUp() throws InterruptedException {
        mActivityTestRule.startMainActivityOnBlankPage();
    }

    @After
    public void teardown() {
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(false);
    }

    /**
     * Tests that ChromeTabbedActivity2 is used for intents when EXTRA_WINDOW_ID is set to 2.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentWithExtraWindowId() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        createSecondChromeTabbedActivity(activity1);

        Intent intent = activity1.getIntent();
        intent.putExtra(IntentHandler.EXTRA_WINDOW_ID, 2);

        Assert.assertEquals(
                "ChromeTabbedActivity2 should be used when EXTRA_WINDOW_ID is set to 2.",
                ChromeTabbedActivity2.class,
                MultiWindowUtils.getInstance().getTabbedActivityForIntent(intent, activity1));
    }

    /**
     * Tests that if two ChromeTabbedActivities are running the one that was resumed most recently
     * is used as the class name for new intents.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentLastResumedActivity() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        final ChromeTabbedActivity2 activity2 = createSecondChromeTabbedActivity(activity1);

        Assert.assertFalse("ChromeTabbedActivity should not be resumed",
                ApplicationStatus.getStateForActivity(activity1) == ActivityState.RESUMED);
        Assert.assertTrue("ChromeTabbedActivity2 should be resumed",
                ApplicationStatus.getStateForActivity(activity2) == ActivityState.RESUMED);

        // Open settings and wait for ChromeTabbedActivity2 to pause.
        TestThreadUtils.runOnUiThreadBlocking(
                () -> { activity2.onMenuOrKeyboardAction(R.id.preferences_id, true); });
        int expected = ActivityState.PAUSED;
        CriteriaHelper.pollUiThread(() -> {
            Criteria.checkThat(
                    ApplicationStatus.getStateForActivity(activity2), Matchers.is(expected));
        });

        Assert.assertEquals(
                "The most recently resumed ChromeTabbedActivity should be used for intents.",
                ChromeTabbedActivity2.class,
                MultiWindowUtils.getInstance().getTabbedActivityForIntent(
                        activity1.getIntent(), activity1));
    }

    /**
     * Tests that if only ChromeTabbedActivity is running it is used as the class name for intents.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentOnlyActivity1IsRunning() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        ChromeTabbedActivity2 activity2 = createSecondChromeTabbedActivity(activity1);
        activity2.finishAndRemoveTask();

        Assert.assertEquals(
                "ChromeTabbedActivity should be used for intents if ChromeTabbedActivity2 is "
                        + "not running.",
                ChromeTabbedActivity.class,
                MultiWindowUtils.getInstance().getTabbedActivityForIntent(
                        activity1.getIntent(), activity1));
    }

    /**
     * Tests that if only ChromeTabbedActivity2 is running it is used as the class name for intents.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentOnlyActivity2IsRunning() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        createSecondChromeTabbedActivity(activity1);
        activity1.finishAndRemoveTask();

        Assert.assertEquals(
                "ChromeTabbedActivity2 should be used for intents if ChromeTabbedActivity is "
                        + "not running.",
                ChromeTabbedActivity2.class,
                MultiWindowUtils.getInstance().getTabbedActivityForIntent(
                        activity1.getIntent(), activity1));
    }

    /**
     * Tests that if no ChromeTabbedActivities are running ChromeTabbedActivity is used as the
     * default for intents.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testTabbedActivityForIntentNoActivitiesAlive() {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        activity1.finishAndRemoveTask();

        Assert.assertEquals(
                "ChromeTabbedActivity should be used as the default for external intents.",
                ChromeTabbedActivity.class,
                MultiWindowUtils.getInstance().getTabbedActivityForIntent(
                        activity1.getIntent(), activity1));
    }

    /**
     * Tests that MultiWindowUtils properly tracks whether ChromeTabbedActivity2 is running.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testTabbedActivity2TaskRunning() {
        ChromeTabbedActivity activity2 =
                createSecondChromeTabbedActivity(mActivityTestRule.getActivity());
        Assert.assertTrue(MultiWindowUtils.getInstance().getTabbedActivity2TaskRunning());

        activity2.finishAndRemoveTask();
        MultiWindowUtils.getInstance().getTabbedActivityForIntent(
                mActivityTestRule.getActivity().getIntent(), mActivityTestRule.getActivity());
        Assert.assertFalse(MultiWindowUtils.getInstance().getTabbedActivity2TaskRunning());
    }

    /**
     * Tests that {@link MultiWindowUtils#areMultipleChromeInstancesRunning} behaves correctly in
     * the case the second instance is killed first.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    public void testAreMultipleChromeInstancesRunningSecondInstanceKilledFirst()
            throws TimeoutException {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
        Assert.assertFalse("Only a single instance should be running at the start.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        CallbackHelper activity1StoppedCallback = new CallbackHelper();
        CallbackHelper activity1DestroyedCallback = new CallbackHelper();
        CallbackHelper activity1ResumedCallback = new CallbackHelper();
        ApplicationStatus.ActivityStateListener activity1StateListener =
                new ApplicationStatus.ActivityStateListener() {
                    @Override
                    public void onActivityStateChange(Activity activity, int newState) {
                        switch (newState) {
                            case ActivityState.STOPPED:
                                activity1StoppedCallback.notifyCalled();
                                break;
                            case ActivityState.DESTROYED:
                                activity1DestroyedCallback.notifyCalled();
                                break;
                            case ActivityState.RESUMED:
                                activity1ResumedCallback.notifyCalled();
                                break;
                        }
                    }
                };
        TestThreadUtils.runOnUiThreadBlocking(() -> {
            ApplicationStatus.registerStateListenerForActivity(activity1StateListener, activity1);
        });

        // Starting activity2 will stop activity1 as this is not truly multi-window mode.
        int activity1CallCount = activity1StoppedCallback.getCallCount();
        ChromeTabbedActivity activity2 = createSecondChromeTabbedActivity(activity1);
        activity1StoppedCallback.waitForCallback(activity1CallCount);
        Assert.assertTrue("Both instances should be running now that the second has started.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        CallbackHelper activity2DestroyedCallback = new CallbackHelper();
        ApplicationStatus.ActivityStateListener activity2StateListener =
                new ApplicationStatus.ActivityStateListener() {
                    @Override
                    public void onActivityStateChange(Activity activity, int newState) {
                        switch (newState) {
                            case ActivityState.DESTROYED:
                                activity2DestroyedCallback.notifyCalled();
                                break;
                        }
                    }
                };
        TestThreadUtils.runOnUiThreadBlocking(() -> {
            ApplicationStatus.registerStateListenerForActivity(activity2StateListener, activity2);
        });

        // activity1 may have been destroyed in the background. After destroying activity2 it is
        // necessary to make sure activity1 gets resumed.
        activity1CallCount = activity1ResumedCallback.getCallCount();
        activity2.finishAndRemoveTask();
        activity2DestroyedCallback.waitForFirst();
        activity1ResumedCallback.waitForCallback(activity1CallCount);
        Assert.assertFalse("Only a single instance should be running after the second is killed.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        // activity1 may have been destroyed in the background and now it is in the foreground.
        // Wait on the next destroyed call rather than the first.
        activity1CallCount = activity1DestroyedCallback.getCallCount();
        activity1.finishAndRemoveTask();
        activity1DestroyedCallback.waitForCallback(activity1CallCount);
        Assert.assertFalse("No instances should be running as all instances are killed.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));
    }

    /**
     * Tests that {@link MultiWindowUtils#areMultipleChromeInstancesRunning} behaves correctly in
     * the case the first instance is killed first.
     *
     * TODO(crbug/1077249): This testcase is restricted to O+ as on Android N calling
     * {@link Activity#finishAndRemoveTask()} on the backgrounded activity1 will not cause it to
     * be DESTROYED it until after activity2 is PAUSED. On O+ activity1 will be DESTROYED
     * immediately. This test should be changed such that it works on N.
     */
    @Test
    @SmallTest
    @Feature("MultiWindow")
    @DisableIf.
    Build(sdk_is_less_than = Build.VERSION_CODES.O, message = "https://crbug.com/1077249")
    public void testAreMultipleChromeInstancesRunningFirstInstanceKilledFirst()
            throws TimeoutException {
        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
        Assert.assertFalse("Only a single instance should be running at the start.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        CallbackHelper activity1StoppedCallback = new CallbackHelper();
        CallbackHelper activity1DestroyedCallback = new CallbackHelper();
        ApplicationStatus.ActivityStateListener activity1StateListener =
                new ApplicationStatus.ActivityStateListener() {
                    @Override
                    public void onActivityStateChange(Activity activity, int newState) {
                        switch (newState) {
                            case ActivityState.STOPPED:
                                activity1StoppedCallback.notifyCalled();
                                break;
                            case ActivityState.DESTROYED:
                                activity1DestroyedCallback.notifyCalled();
                                break;
                        }
                    }
                };
        TestThreadUtils.runOnUiThreadBlocking(() -> {
            ApplicationStatus.registerStateListenerForActivity(activity1StateListener, activity1);
        });

        // Starting activity2 will stop activity1 as this is not truly multi-window mode.
        // activity1 may be killed in the background, but since it is never foregrounded again
        // there should be only one call for both stopped and destroyed in this test.
        ChromeTabbedActivity activity2 = createSecondChromeTabbedActivity(activity1);
        activity1StoppedCallback.waitForFirst();
        Assert.assertTrue("Both instances should be running now that the second has started.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity1));

        CallbackHelper activity2DestroyedCallback = new CallbackHelper();
        ApplicationStatus.ActivityStateListener activity2StateListener =
                new ApplicationStatus.ActivityStateListener() {
                    @Override
                    public void onActivityStateChange(Activity activity, int newState) {
                        switch (newState) {
                            case ActivityState.DESTROYED:
                                activity2DestroyedCallback.notifyCalled();
                                break;
                        }
                    }
                };
        TestThreadUtils.runOnUiThreadBlocking(() -> {
            ApplicationStatus.registerStateListenerForActivity(activity2StateListener, activity2);
        });

        activity1.finishAndRemoveTask();
        activity1DestroyedCallback.waitForFirst();
        Assert.assertFalse("Only a single instance should be running after the first is killed.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity2));

        // activity2 is always in the foreground so this should be the first time it is destroyed.
        activity2.finishAndRemoveTask();
        activity2DestroyedCallback.waitForFirst();
        Assert.assertFalse("No instances should be running as all instances are killed.",
                MultiWindowUtils.getInstance().areMultipleChromeInstancesRunning(activity2));
    }
}
